package liquidfs.core;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.logging.*;

import javax.xml.parsers.*;
import javax.xml.stream.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;

import org.w3c.dom.*;
import org.xml.sax.*;

import utility.*;

import liquidfs.*;
import liquidfs.data.*;
import liquidfs.data.fs.*;
import liquidfs.data.ops.*;
import liquidfs.ui.net.*;

import com.siemens.ct.exi.*;
import com.siemens.ct.exi.exceptions.*;
import com.siemens.ct.exi.grammars.*;
import com.siemens.ct.exi.helpers.*;
import com.thoughtworks.xstream.*;
import com.thoughtworks.xstream.converters.*;
import com.thoughtworks.xstream.converters.collections.*;
import com.thoughtworks.xstream.converters.extended.*;
import com.thoughtworks.xstream.converters.reflection.*;
import com.thoughtworks.xstream.io.*;
import com.thoughtworks.xstream.io.xml.*;

public class Serializer
{ 
   private XStream xstream;
   private WriterFactory writerFactory;
   
   private static Map<Application, XStream> serializers = new HashMap<Application, XStream>();
   private static Logger log = Logger.getLogger("liquidfs.core.Serializer");
   private static EXIFactory exiFactory = null;
   
   private Serializer (XStream xstream, WriterFactory factory)
   {
      this.xstream = xstream;
      this.writerFactory = factory;
   }
   
   private static XStream buildXStream (Application app) {
      
      // Tells stream to use the ultra powerful converter
      XStream xstream = new XStream(new Sun14ReflectionProvider());
      
      // Tell xstream not to use references
      xstream.setMode(XStream.NO_REFERENCES);
      
      // Register a different converter for tree sets that doesn't output <no-comparator/>
      xstream.registerConverter(new CollectionConverter(xstream.getMapper()){
         @SuppressWarnings("rawtypes")
         @Override
         public boolean canConvert(Class type) {
            return type == TreeSet.class || type == ArrayBlockingQueue.class;
         }
      });
      
      // Use the ISO8601 format for converting dates, as this is what XML uses natively, so EXI will recognize it and encode it efficiently
      xstream.registerConverter(new ISO8601DateConverter());
      
      // Declare TreeSet as the default implementation of a Set
      xstream.addDefaultImplementation(TreeSet.class, Set.class);
      
      // Replace the 'class' attribute generated by xstream with xsi:type
      xstream.aliasSystemAttribute("xsi:type", "class");
      
      // Process classes that have XStream annotations
      xstream.processAnnotations(liquidfs.Configuration.class);
      
      // liquidfs.data
//      xstream.processAnnotations(liquidfs.data.ExceptionResult.class);
//      xstream.processAnnotations(liquidfs.data.Host.class);
//      xstream.processAnnotations(liquidfs.data.Link.class);
//      xstream.processAnnotations(liquidfs.data.Notification.class);
//      xstream.processAnnotations(liquidfs.data.Operation.class);
//      xstream.processAnnotations(liquidfs.data.PeerDevice.class);
      xstream.processAnnotations(liquidfs.data.Result.class);
//      xstream.processAnnotations(liquidfs.data.VoidResult.class);
      
      xstream.processAnnotations(liquidfs.data.fs.LocalLiquidFile.class);
      xstream.processAnnotations(liquidfs.data.fs.LocalLiquidDirectory.class);
      xstream.processAnnotations(liquidfs.data.ops.AbstractOperation.class);
      xstream.processAnnotations(liquidfs.ui.net.Message.class);
      xstream.processAnnotations(liquidfs.ui.net.OperationMessage.class);
      xstream.processAnnotations(liquidfs.ui.net.ResultMessage.class);
      
      xstream.registerConverter(new LocalLiquidObject.LocalLiquidObjectSerializer(app));
      
      if (exiFactory == null) {
         try {
            SchemaGenerator schemaGenerator = new SchemaGenerator(xstream);
            schemaGenerator.addClass(liquidfs.data.Revision.class);
            schemaGenerator.addClass(liquidfs.ui.net.OperationMessage.class);
            schemaGenerator.addClass(liquidfs.data.ops.WriteFileOperation.class);
            
            GrammarFactory grammarFactory = GrammarFactory.newInstance();
            Grammars schemaGrammar = grammarFactory.createGrammars(schemaGenerator.getSchemaInputStream());
            
            // Build the EXI writer
            EXIFactory factory = DefaultEXIFactory.newInstance();
            
            // Augment the writer with schema information, if available
            factory.setGrammars(schemaGrammar);
            
            // Set some encoding options
            EncodingOptions options = EncodingOptions.createDefault();
            options.setOption(EncodingOptions.INCLUDE_XSI_SCHEMALOCATION);
            factory.setEncodingOptions(options);
            
            // Set fidelity options
            FidelityOptions fidelity = FidelityOptions.createDefault();
            factory.setFidelityOptions(fidelity);
            
            Serializer.exiFactory = factory;
         }
         catch (Exception e) {
            exiFactory = null;
         }
      }
      
      return xstream;
   }
   
   public static Serializer getDefaultSerializer (Application app)
   {
	   if (!Serializer.serializers.containsKey(app))
		   Serializer.serializers.put(app, buildXStream(app));
	   
	   XStream xstream = Serializer.serializers.get(app);
	   
	   if (app.getConfiguration().isXmlCompressionEnabled())
	      return new Serializer(xstream, new ExiWriterFactory(exiFactory));
	   else
	      return new Serializer(xstream, new XmlWriterFactory());
   }
   
   public static Serializer getPlainSerializer (Application app)
   {
      if (!Serializer.serializers.containsKey(app))
         Serializer.serializers.put(app, buildXStream(app));
      
      return new Serializer(Serializer.serializers.get(app), new XmlWriterFactory());
   }
   
   public void preprocess (Class<?> c) {
      xstream.processAnnotations(c);
   }
   
   public String serialize (Object object)
   {
      StringWriter writer = new StringWriter();
      xstream.marshal(object, new PrettyPrintWriter(writer));
      return writer.toString();
   }
   
   public void serialize (Object object, OutputStream stream)
   {
      xstream.marshal(object, writerFactory.createWriter(stream));
   }
   
   public Object deserialize (String string)
   {
      return xstream.fromXML(string);
   }
   
   public Object deserialize (InputStream stream) throws IOException
   {
      HierarchicalStreamReader reader;
      
      // Wrap the input stream in a pushback input stream so we can examine it before passing it on to the XML parser
      PushbackInputStream in = new PushbackInputStream(stream);
      
      // Read the first byte, to determine whether this is a EXI (compress binary XML) document, or a plaintext XML document
      byte b0 = (byte) in.read();
      
      // Look for the start of the "$EXI" cookie (if present)
      if (b0 == '$') {
         
         // Since we saw the '$', read the next three bytes
         byte b1 = (byte) in.read();
         byte b2 = (byte) in.read();
         byte b3 = (byte) in.read();
         
         // If they match, read one byte more, and we'll check to see if it contains the Distinguishing Bits (in the next clause)
         if (b1 == 'E' && b2 == 'X' && b3 == 'I') {
            b0 = (byte) in.read();
         }
         else {
            // If not, put them back in the stream
            in.unread(b3);
            in.unread(b2);
            in.unread(b1);
         }
      }
      
      // But the first byte we read back in the stream
      in.unread(b0);
      
      // Look for the two "distinguishing bits" (10xxxxxx) of the EXI header
      if ((b0 & 0x80) != 0 && (b0 & 0x40) == 0) {
         try {
            reader = new ExiReader(in);
         } 
         catch (EXIException e) {
            throw new IOException("Stream appeared to be an EXI stream, but was not formatted properly: " + e.getMessage(), e);
         } 
         catch (XMLStreamException e) {
            throw new IOException("Problem encountered reading EXI stream: " + e.getMessage(), e);
         }
      }
      else {
         try {
            reader = new XppReader(new InputStreamReader(in, "UTF-8"));
         }
         catch (UnsupportedEncodingException e) {
            reader = new XppReader(new InputStreamReader(in));
         }
      }
      
      // Unmarshal the object from the stream
      return xstream.unmarshal(reader);
   }
   
   public static interface WriterFactory {
      public HierarchicalStreamWriter createWriter (OutputStream stream);
   }
   
   public static class XmlWriterFactory implements WriterFactory {
      public HierarchicalStreamWriter createWriter (OutputStream stream) {
         return new XmlWriter(stream);
      }
   }
   
   private static class ExiWriterFactory implements WriterFactory {
      
      EXIFactory factory;
      public ExiWriterFactory (EXIFactory factory) {
         this.factory = factory;
      }
      
      public HierarchicalStreamWriter createWriter (OutputStream stream) {
         try {
            return new ExiWriter(factory, stream);
         } 
         catch (Exception e) {
            try {
               return new PrettyPrintWriter(new OutputStreamWriter(stream, "UTF-8"));
            } 
            catch (UnsupportedEncodingException f) {
               return new PrettyPrintWriter(new OutputStreamWriter(stream));
            }
         }
      }
   }
}
